﻿namespace PugLib.UnitTest
{
	using System;
	using System.Diagnostics;
	using System.IO;
	using System.Net;
	using System.Threading;

	using Microsoft.VisualStudio.TestTools.UnitTesting;

	using PugLib.Collections;

	/// <summary>
	/// Unit tests against Memoize with ValidationDictionary.
	/// </summary>
	[TestClass]
	public class MemoizeValidationTest
	{
		/// <summary>
		/// Memoize a function and apply a predicate that always passes.
		/// </summary>
		[TestMethod]
		public void AlwaysValidTest()
		{
			ValidationDictionary<string, string> dict = new ValidationDictionary<string, string>(x => true);
			int counter = 0;
			Func<string, string> func = delegate(string x)
			                            {
			                            	counter++;
			                            	return x.GetHashCode().ToString();
			                            };
			Func<string, string> memoizedFunc = Memos.Memoize(func, dict);

			// Memoize keeps everything valid unconditionally
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter, "Function counter mismatch");
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter++
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter++
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter++
			Assert.AreEqual(5, counter, "Function counter mismatch");
		}

		/// <summary>
		/// Memoize a function and apply a predicate that always fails.
		/// </summary>
		[TestMethod]
		public void AlwaysInvalidTest()
		{
			ValidationDictionary<string, string> dict = new ValidationDictionary<string, string>(x => false);
			int counter = 0;
			Func<string, string> func = delegate(string x)
			                            {
			                            	counter++;
			                            	return x.GetHashCode().ToString();
			                            };
			Func<string, string> memoizedFunc = Memos.Memoize(func, dict);

			// Everything always invalidated
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter, "Function counter mismatch");
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(8, counter, "Function counter mismatch");
		}

		/// <summary>
		/// Memoize a function and apply a global timeout that expires in a short amount of time.
		/// </summary>
		[TestMethod]
		public void GlobalTimeoutValidTest()
		{
			TimeSpan timeoutDuration = TimeSpan.FromMilliseconds(250);
			DateTime expirationTime = DateTime.Now + timeoutDuration;
			ValidationDictionary<string, string> dict = new ValidationDictionary<string, string>(x => DateTime.Now < expirationTime);
			int[] counter = {0};
			Func<string, string> func = x =>
			                            {
			                            	counter[0]++;
			                            	return x.GetHashCode().ToString();
			                            };
			Func<string, string> memoizedFunc = Memos.Memoize(func, dict);

			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter[0], "Function counter mismatch");
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter++
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter++
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter++
			Assert.AreEqual(5, counter[0], "Function counter mismatch");

			// Sleep until timeout
			Thread.Sleep(timeoutDuration.Add(TimeSpan.FromMilliseconds(250))); // buffer add'l 250ms

			// Timeout expired, so everything stays invalidated
			counter[0] = 0;
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter[0], "Function counter mismatch");
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(8, counter[0], "Function counter mismatch");
		}

		/// <summary>
		/// Memoize a function and apply a global timeout that expires immediately.
		/// </summary>
		[TestMethod]
		public void GlobalTimeoutInvalidTest()
		{
			DateTime expirationTime = DateTime.Now;
			ValidationDictionary<string, string> dict = new ValidationDictionary<string, string>(x => DateTime.Now < expirationTime);
			int counter = 0;
			Func<string, string> func = delegate(string x)
			                            {
			                            	counter++;
			                            	return x.GetHashCode().ToString();
			                            };
			Func<string, string> memoizedFunc = Memos.Memoize(func, dict);

			// Timeout expires immediately, so everything stays invalidated
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter, "Function counter mismatch");
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(func("a"), memoizedFunc("a"), "Function value mismatch"); // counter += 2
			Assert.AreEqual(8, counter, "Function counter mismatch");
		}

		/// <summary>
		/// Memoize a function and apply a per-value timeout.
		/// This uses the Timeoutable generic as a value type to assign each
		/// value a timeout and perform validation against it.
		/// </summary>
		[TestMethod]
		public void TimeoutableTest()
		{
			TimeSpan timeoutDuration = TimeSpan.FromMilliseconds(250);
			ValidationDictionary<string, Timeoutable<string>> dict = null;
			dict = new ValidationDictionary<string, Timeoutable<string>>(pair => pair.Value.IsValid());
			int counter = 0;
			Func<string, Timeoutable<string>> func = delegate(string x)
			                                         {
			                                         	counter++;
			                                         	return new Timeoutable<string>(x.GetHashCode().ToString(), timeoutDuration);
			                                         };
			Func<string, Timeoutable<string>> memoizedFunc = Memos.Memoize(func, dict);

			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter, "Function counter mismatch");
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter++
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter++
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter++
			Assert.AreEqual(5, counter, "Function counter mismatch");

			// Sleep until timeout
			Thread.Sleep(timeoutDuration.Add(TimeSpan.FromMilliseconds(250))); // buffer add'l 250ms

			// Timeout expired, so next call is invalidated, timeout reset
			counter = 0;
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter += 2
			Assert.AreEqual(2, counter, "Function counter mismatch");
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter++
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter++
			Assert.AreEqual(func("a").Value, memoizedFunc("a").Value, "Function value mismatch"); // counter++
			Assert.AreEqual(5, counter, "Function counter mismatch");
		}

		/// <summary>
		/// Memoize a web request with timeout
		/// </summary>
		[TestMethod]
		public void WebRequestTimeoutTest()
		{
			TimeSpan timeoutDuration = TimeSpan.FromMilliseconds(500);
			ValidationDictionary<Uri, IValidatable<string>> dict = new ValidationDictionary<Uri, IValidatable<string>>(pair => pair.Value.IsValid());
			int counter = 0;

			Func<Uri, IValidatable<string>> func = delegate(Uri requestUri)
			                                       {
			                                       	HttpWebResponse response = (HttpWebResponse)WebRequest.Create(requestUri).GetResponse();
			                                       	string content = new StreamReader(response.GetResponseStream()).ReadToEnd();
			                                       	counter++;
			                                       	return new Timeoutable<string>(content, timeoutDuration);
			                                       };

			Func<Uri, IValidatable<string>> memoizedFunc = Memos.Memoize(func, dict);

			Uri[] urlList = new[]
			                {
			                	new Uri("http://feeds.digg.com/digg/popular.rss"),
			                	new Uri("http://stackoverflow.com/feeds"),
			                	new Uri("http://news.google.com/news?pz=1&cf=all&ned=us&hl=en&topic=h&num=3&output=rss")
			                };

			foreach (Uri url in urlList)
			{
				string content1 = string.Empty;
				counter = 0;

				// Call web request a few times, should make only one actual web request
				for (int i = 0; i < 3; i++)
				{
					content1 = memoizedFunc(url).Value;
				}
				Assert.AreEqual(1, counter, "Function counter mismatch");

				// Sleep until timeout
				Thread.Sleep(timeoutDuration + TimeSpan.FromMilliseconds(250)); // buffer add'l 250ms

				// Timeout expired, so next call is invalidated, timeout reset
				counter = 0;
				for (int i = 0; i < 3; i++)
				{
					IValidatable<string> content2 = memoizedFunc(url);
				}
				Assert.AreEqual(1, counter, "Function counter mismatch");

				// Show response content
				Debug.WriteLine(string.Format("Request Uri: {0}", url));
				Debug.WriteLine(string.Format("Content: ({1} bytes){0}{2}...{0}---{0}{0}", Environment.NewLine, content1.Length, content1.Substring(0, Math.Min(content1.Length, 1024))));
			}
		}
	}
}